const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");
const { argv } = require("yargs");
const pinyin = require("pinyin").pinyin;
const {
    flatten,
    removeDuplicate,
    removeDuplicateObject,
    convert,
    formatDate,
    formatTimeInMillisec,
    formatTimestampMillisec,
    format,
} = require("../utils/transform_tool");
const { parse_sync, stringify_sync } = require("../utils/parse_tool");
const { parseDirective } = require("../utils/parse_directive");
const { hash } = require("../utils/hash_tool");
const now = Date.now();
const argv0 = process.argv0;
let projectRoot = path.resolve(__dirname, "../");
if (path.basename(argv0).startsWith("automator")) {
    projectRoot = path.dirname(argv0);
}
const shareData = {
    // 本程序所在目录
    AUTOMATOR_ROOT: path.resolve(projectRoot, "./"),
    AUTOMATOR_SCRATCH: path.resolve(projectRoot, "./scratch"),
    AUTOMATOR_LOG: path.resolve(projectRoot, "./log"),
    AUTOMATOR_CFG: path.resolve(projectRoot, "./cfg"),
    AUTOMATOR_APPDATA: require("persist-path")("TsubasaYeung/Automator"),
    AUTOMATOR_BEGIN_TIME: now,
    AUTOMATOR_BEGIN_FORMATED_TIME: formatTimestampMillisec(
        now,
        "YYYYMMDD_hhmmss",
    ),
    AUTOMATOR_ELAPSED_TIME: 0,
    // 运行的工作目录
    CWD: process.cwd(),
    // 环境变量
    ENV: process.env,
    // 命令行参数
    ARGV: argv,
    // 是否步进执行
    STEP_BY_STEP: 0,
    // 是否全自动执行
    FULL_AUTOMATIC: 0,
    QUIET_MODE: 0,
    DEBUG_MODE: 0,
    $COMPRESS_PROGERSS: 0,
    // 指向 shareData 自身
    SHARE_DATA: undefined,
    // 干跑
    DRY_RUN: 0,
    /** 是否在所有命令执行完毕后立即退出，不等待定时器等异步逻辑 */
    EXIT_IMMEDIATELY: 0,
    processPipes: processPipes,
};
shareData.SHARE_DATA = shareData;
Object.defineProperty(shareData, "AUTOMATOR_ELAPSED_TIME", {
    get() {
        return Date.now() - this.AUTOMATOR_BEGIN_TIME;
    },
});

const LINE_EXPRESSION = "\\r\\n|\\n\\r|\\n|\\r";

// 使用 argv 更新 shareData
const rawArgs = {};
let tmpkey = null;
for (let i = 2, len = process.argv.length; i < len; ++i) {
    const curarg = process.argv[i];
    if (curarg[0] === "-") {
        tmpkey = curarg.substr(curarg[1] === "-" ? 2 : 1);
        rawArgs[tmpkey] = true;
    } else {
        if (tmpkey != null) {
            rawArgs[tmpkey] = curarg;
            tmpkey = null;
        } else {
        }
    }
}
for (const k in argv) {
    if (k !== "_" && k[0] !== "$") {
        if (shareData[k] != null) {
            shareData[k] = argv[k];
        } else {
            shareData[k] = rawArgs[k];
        }
    }
}

/**
 *
 * @param {string} data
 */
function isSingular(data) {
    if (!/^<<.*>>$/.test(data)) {
        return false;
    }
    const reg = /(?:<<)|(?:>>)/g;
    let matched;
    let isRight = false;
    while ((matched = reg.exec(data))) {
        if (isRight && matched[0] === "<<") {
            return false;
        }
        if (!isRight && matched[0] === ">>") {
            isRight = true;
        }
    }
    return true;
}

function formatData(data) {
    if (typeof data === "string") {
        let reg; // = /^<<([^\$<>]*?)>>$/g;
        let match; // = reg.exec(data);
        if (isSingular(data)) {
            return getDataByKey(data.substring(2, data.length - 2));
            // let ret = data.substring(2, data.length - 2);
            // return getDataByKey(ret);
        } else {
            // ?:  is for non capturing group
            // ?=  is for positive look ahead
            // ?!  is for negative look ahead
            // ?<= is for positive look behind
            // ?<! is for negative look behind
            reg = /(?<!\$)<<([^$<>]*?)>>/g;
            // 处理嵌套 字符串
            while ((match = data.match(reg))) {
                data = data.replace(reg, (...args) => {
                    return getDataByKey(args[1]);
                });
            }
            // 处理嵌套 代码
            reg = /\$<<([^$]*?)>>/g;
            while ((match = data.match(reg))) {
                data = data.replace(reg, (...args) => {
                    return `this.${args[1]}`;
                });
            }
        }
    } else if (Array.isArray(data)) {
        for (let i = 0, len = data.length; i < len; ++i) {
            data[i] = formatData(data[i]);
        }
    } else if (typeof data === "object") {
        for (const key in data) {
            // 循环体内不处理
            if (key === "commands") continue;
            const value = data[key];
            data[key] = formatData(value);
        }
    }
    return data;
}

/**
 * 获取完整真实路径
 * @param {*} src
 */
function get_full_path(src, createIfNotExist) {
    const dstDir = path
        .resolve(src.replace(/^~/, os.homedir()))
        .replace(/\\/g, "/");
    let targetDir;
    switch (createIfNotExist) {
        case "FILE": {
            targetDir = path.dirname(dstDir);
            break;
        }
        case "FOLDER": {
            targetDir = dstDir;
            break;
        }
    }
    if (
        targetDir &&
        (!fs.existsSync(targetDir) || !fs.lstatSync(targetDir).isDirectory())
    ) {
        fs.mkdirSync(targetDir, {
            recursive: true,
        });
    }
    return dstDir;
}

function call_code(code) {
    return eval(`(${code})`);
}

function eval_code(code) {
    if (code == null) {
        return null;
    }
    return call_code.call(shareData, `(${code})`);
}

/**
 *
 * @param {string} key
 */
function getDataByKey(key, from = shareData) {
    key = key.toString();
    const first_pipe_index = key.indexOf("|");
    const subkeys = (
        first_pipe_index === -1
            ? key
            : key.substring(0, first_pipe_index).trim()
    ).split(".");
    let val = from;
    for (const subkey of subkeys) {
        if (subkey == null) {
            continue;
        }
        val = val?.[formatData(subkey)];
    }
    if (first_pipe_index !== -1) {
        let rawstr = key.substring(first_pipe_index + 1);
        rawstr = formatData(rawstr);
        val = processPipes(val, rawstr);
    }
    return val;
}

function processPipe(val, pipename, pipeargs) {
    switch (pipename) {
        case "istream": {
            if (pipeargs[0]) {
                val = fs.ReadStream.from(val);
            } else {
                val = fs.createReadStream(val);
            }
            break;
        }
        case "ostream": {
            val = fs.createWriteStream(val);
            break;
        }
        case "splitlines": {
            val = val.split(new RegExp(LINE_EXPRESSION));
            break;
        }
        case "split": {
            val = val.split(pipeargs[0]);
            break;
        }
        case "join": {
            val = val.join(pipeargs[0]);
            break;
        }
        case "flatten": {
            val = flatten(val);
            break;
        }
        case "unique": {
            val = removeDuplicate(val);
            break;
        }
        case "unique_object": {
            val = removeDuplicateObject(val, pipeargs[0]);
            break;
        }
        case "remove_empty": {
            val = val.filter((v) => v);
            break;
        }
        case "sort": {
            let sortBy = pipeargs[0];
            let sortFactor = 1;
            const sortProps = pipeargs[1];
            if (sortBy && sortBy[0] === "-") {
                sortBy = sortBy.substring(1);
                sortFactor = -1;
            }
            let compareFn = (a, b) => {
                if (sortProps) {
                    a = getDataByKey(sortProps, a);
                    b = getDataByKey(sortProps, b);
                }
                let ret = 0;
                if (a > b) ret = 1;
                else if (a < b) ret = -1;
                return ret * sortFactor;
            };
            switch (sortBy) {
                case "birthtime": {
                    compareFn = (a, b) => {
                        if (sortProps) {
                            a = getDataByKey(sortProps, a);
                            b = getDataByKey(sortProps, b);
                        }
                        return (
                            sortFactor *
                            (fs.statSync(a).birthtimeMs -
                                fs.statSync(b).birthtimeMs)
                        );
                    };
                    break;
                }
                // atime	Access Time	访问时间
                // 最后一次访问文件（读取或执行）的时间
                case "atime": {
                    compareFn = (a, b) => {
                        if (sortProps) {
                            a = getDataByKey(sortProps, a);
                            b = getDataByKey(sortProps, b);
                        }
                        return (
                            sortFactor *
                            (fs.statSync(a).atimeMs - fs.statSync(b).atimeMs)
                        );
                    };
                    break;
                }
                // ctime	Change Time	变化时间
                // 最后一次改变文件（属性或权限）或者目录（属性或权限）的时间
                case "ctime": {
                    compareFn = (a, b) => {
                        if (sortProps) {
                            a = getDataByKey(sortProps, a);
                            b = getDataByKey(sortProps, b);
                        }
                        return (
                            sortFactor *
                            (fs.statSync(a).ctimeMs - fs.statSync(b).ctimeMs)
                        );
                    };
                    break;
                }
                // mtime	Modify Time	修改时间
                // 最后一次修改文件（内容）或者目录（内容）的时间
                case "mtime": {
                    compareFn = (a, b) => {
                        if (sortProps) {
                            a = getDataByKey(sortProps, a);
                            b = getDataByKey(sortProps, b);
                        }
                        return (
                            sortFactor *
                            (fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs)
                        );
                    };
                    break;
                }
                case "size": {
                    compareFn = (a, b) => {
                        if (sortProps) {
                            a = getDataByKey(sortProps, a);
                            b = getDataByKey(sortProps, b);
                        }
                        return (
                            sortFactor *
                            (fs.statSync(a).size - fs.statSync(b).size)
                        );
                    };
                    break;
                }
                case "length": {
                    compareFn = (a, b) => {
                        if (sortProps) {
                            a = getDataByKey(sortProps, a);
                            b = getDataByKey(sortProps, b);
                        }
                        return sortFactor * (a.length - b.length);
                    };
                    break;
                }
            }
            val = Array.prototype.sort.call(val, compareFn);
            break;
        }
        case "parse": {
            val = parse_sync(val, pipeargs[0]);
            break;
        }
        case "stringify": {
            val = stringify_sync(val, pipeargs[0]);
            break;
        }
        case "convert": {
            val = convert(val, pipeargs[0]);
            break;
        }
        case "def": {
            val = val == null ? pipeargs[0] : val;
            break;
        }
        case "first": {
            val = val?.[0];
            break;
        }
        case "last": {
            val = val?.[val?.length - 1];
            break;
        }
        case "property": {
            for (const property of pipeargs) {
                val = val?.[property];
            }
            break;
        }
        case "now_date": {
            val = new Date();
            break;
        }
        case "now_timestamp": {
            val = Date.now();
            break;
        }
        case "format": {
            val = formatData(val);
            break;
        }
        case "format_date": {
            val = formatDate(val, pipeargs?.[0]);
            break;
        }
        case "format_timestamp": {
            val = formatTimestampMillisec(val, pipeargs?.[0]);
            break;
        }
        case "format_time": {
            val = formatTimeInMillisec(val, pipeargs?.[0]);
            break;
        }
        case "sequence": {
            switch (pipeargs?.[0]) {
                case "date": {
                    const date = new Date(val);
                    const dateTime = date.getTime();
                    val = [];
                    const repeat = pipeargs?.[1] || 1;
                    const step = pipeargs?.[2] || 1;
                    const fmt = pipeargs?.[3] || "YYYY-MM-DD";
                    for (let i = 0; i < repeat * step; i += step) {
                        date.setTime(dateTime + i * 24 * 3600 * 1000);
                        val.push(formatDate(date, fmt));
                    }
                    break;
                }
                case "number": {
                    const beg = val;
                    val = [];
                    const repeat = pipeargs?.[1] || 1;
                    const step = pipeargs?.[2] || 1;
                    for (let i = 0; i < repeat * step; i += step) {
                        val.push(beg + i);
                    }
                    break;
                }
                default: {
                    break;
                }
            }
            break;
        }
        case "value2key": {
            const tmp = {};
            for (const k in val) {
                tmp[val[k]] = k;
            }
            val = tmp;
            break;
        }
        case "step": {
            const interval = pipeargs[0];
            const offset = pipeargs[1] || 0;
            val = (((val / interval) << 0) + offset) * interval;
            break;
        }
        case "merge": {
            const k_primary = pipeargs[0];
            const k_calculate = pipeargs[1];
            const filter_zero = pipeargs[2];
            const resolved = {};
            let result = [];
            for (const v of val) {
                v[k_calculate] = +v[k_calculate];
                const v_primary = v[k_primary];
                if (resolved[v_primary]) {
                    resolved[v_primary][k_calculate] += v[k_calculate];
                } else {
                    resolved[v_primary] = {
                        [k_primary]: v_primary,
                        [k_calculate]: v[k_calculate],
                    };
                    result.push(resolved[v_primary]);
                }
            }
            if (filter_zero) {
                result = result.filter((v) => v[k_calculate]);
            }
            val = result;
            break;
        }
        case "child": {
            val = getDataByKey(pipeargs[0], val);
            break;
        }
        case "retrieve": {
            val = getDataByKey(val);
            break;
        }
        case "common_dirname": {
            let dir = null;
            for (const file of val) {
                const tmp = path.dirname(file);
                if (dir == null || dir.length > tmp.length) {
                    dir = tmp;
                }
            }
            val = dir;
            break;
        }
        case "slice": {
            val = val.slice(pipeargs[0], pipeargs[1]);
            break;
        }
        case "reverse": {
            val = val.map((_v, i, arr) => arr[arr.length - 1 - i]);
            break;
        }
        case "map": {
            val = val.map((v) => v[pipeargs[0]]);
            break;
        }
        case "chunk": {
            var ret = [];
            var chunkSize = pipeargs[0] || 50;
            for (
                var i = 0, chunkNum = Math.ceil(val.length / chunkSize);
                i < chunkNum;
                i++
            ) {
                ret.push(val.slice(i * chunkSize, (i + 1) * chunkSize));
            }
            val = ret;
            break;
        }
        case "size": {
            if (!val) {
                val = 0;
            } else {
                if (Array.isArray(val) || typeof val === "string") {
                    val = val.length;
                } else {
                    val = Object.getOwnPropertyNames(val).length;
                }
            }
            break;
        }
        default: {
            if (Array.isArray(val)) {
                val = val.map((v) => processPipe(v, pipename, pipeargs));
            } else {
                switch (pipename) {
                    case "pinyin": {
                        const style = pipeargs[0] || pinyin.STYLE_TONE2;
                        const mode = pipeargs[1] || pinyin.MODE_NORMAL;
                        val = pinyin(val, { style, mode }).map((v) => v[0]);
                        break;
                    }
                    case "ctime": {
                        return fs.statSync(val).ctimeMs;
                    }
                    case "path": {
                        val = get_full_path(val);
                        break;
                    }
                    case "basename": {
                        val = path.basename(val);
                        break;
                    }
                    case "extname": {
                        val = path.extname(val);
                        break;
                    }
                    case "basename_noext": {
                        val = path.basename(val, path.extname(val));
                        break;
                    }
                    case "dirname": {
                        val = path.dirname(val);
                        break;
                    }
                    case "relative": {
                        val = path.relative(pipeargs[0], val);
                        break;
                    }
                    case "path_style": {
                        switch (pipeargs[0]) {
                            case "windows": {
                                val = val.replace(/\//g, "\\");
                                break;
                            }
                            default: {
                                val = val.replace(/\\/g, "/");
                                break;
                            }
                        }
                        break;
                    }
                    case "eval": {
                        val = eval_code(val);
                        break;
                    }
                    case "trim": {
                        val = val.trim();
                        break;
                    }
                    case "host": {
                        try {
                            val = new URL(val).host;
                        } catch (_err) {
                            val = "";
                        }
                        break;
                    }
                    case "hostname": {
                        try {
                            val = new URL(val).hostname;
                        } catch (_err) {
                            val = "";
                        }
                        break;
                    }
                    case "protocol": {
                        try {
                            val = new URL(val).protocol;
                        } catch (_err) {
                            val = "";
                        }
                        break;
                    }
                    case "port": {
                        try {
                            val = new URL(val).port;
                        } catch (_err) {
                            val = "";
                        }
                        break;
                    }
                    case "pathname": {
                        try {
                            val = new URL(val).pathname;
                        } catch (_err) {
                            val = "";
                        }
                        break;
                    }
                    case "query": {
                        try {
                            val = new URL(val).search;
                        } catch (_err) {
                            val = "";
                        }
                        break;
                    }
                    case "decolor": {
                        val = val.replace(/\[\d+m/g, "");
                        break;
                    }
                    case "mkdir": {
                        if (!fs.existsSync(val)) {
                            fs.mkdirSync(val, {
                                recursive: true,
                            });
                        }
                        break;
                    }
                    case "case": {
                        val = require("change-case")[`${pipeargs[0]}Case`](val);
                        break;
                    }
                    case "calc": {
                        switch (pipeargs[0]) {
                            case "+": {
                                val += pipeargs[1];
                                break;
                            }
                            case "-": {
                                val -= pipeargs[1];
                                break;
                            }
                            case "*": {
                                val *= pipeargs[1];
                                break;
                            }
                            case "/": {
                                val /= pipeargs[1];
                                break;
                            }
                            case "%": {
                                val %= pipeargs[1];
                                break;
                            }
                            case "**": {
                                val **= pipeargs[1];
                                break;
                            }
                        }
                        break;
                    }
                    case "buffer": {
                        val = Buffer.from(val);
                        break;
                    }
                    case "btoa": {
                        val = btoa(val);
                        break;
                    }
                    case "atob": {
                        val = atob(val);
                        break;
                    }
                    case "increase": {
                        const offset = pipeargs[0] || 1;
                        if (typeof val === "string") {
                            let index = pipeargs[1] || 0;
                            if (index < 0) {
                                const total =
                                    (val.match(/\D+/g)?.length || 0) + 1;
                                index += total;
                            }
                            const pos = index + 1;
                            let cur = 0;
                            val = val.replace(/\d+/g, (substring) => {
                                cur++;
                                if (cur === pos) {
                                    return +substring + offset;
                                } else {
                                    return substring;
                                }
                            });
                        } else {
                            val = +val + offset;
                        }
                        break;
                    }
                    case "hash": {
                        val = hash(val, pipeargs[0], pipeargs[1]);
                        break;
                    }
                    default:
                        {
                            throw `Unkown pipe "${pipename}"`;
                            break;
                        }
                        break;
                }
            }
        }
    }
    return val;
}

function processPipes(val, exp) {
    const filters = parseDirective(exp);
    for (let i = 0, len = filters.length; i < len; ++i) {
        const filter = filters[i];
        val = processPipe(
            val,
            filter.name,
            filter.args.map((arg) =>
                arg.dynamic ? getDynamicVal(arg.value) : arg.value,
            ),
        );
    }
    return val;
}

const dynamicMap = {
    true: true,
    false: false,
};
function getDynamicVal(val) {
    const mapped = dynamicMap[val];
    if (mapped !== undefined) return mapped;
    return getDataByKey(val);
}

module.exports = {
    shareData,
    stdout_columns: process.stdout.columns || 500,
    clear_line: "\x1B[2K",
    clear_line_end: "\x1B[0K",
    clear_screen: "\x1B[2J",
    OPTION_PAGE_SIZE: 5,
    LINE_EXPRESSION,
    formatData,
    getDataByKey,
    get_full_path,
    eval_code,
};
